{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<img src=\"../common/rfsoc_book_banner.jpg\" alt=\"University of Strathclyde\" align=\"left\">"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<div class=\"alert alert-block\" style=\"background-color: #c7b8d6; padding: 10px\">\n",
    "    <p style=\"color: #222222\">\n",
    "        <b>Note:</b>\n",
    "        <br>\n",
    "        This Jupyter notebook uses hardware features of the Zynq UltraScale+ RFSoC device. Therefore, the notebook cells will only execute successfully on an RFSoC platform.\n",
    "    </p>\n",
    "</div>\n",
    "\n",
    "# Notebook Set A\n",
    "\n",
    "---\n",
    "\n",
    "## 04 - Overlays and Hardware Interfacing\n",
    "This notebook will expand your understanding of PYNQ overlays by investigating a simple overlay design on RFSoC. The overlay design consists of a Numerically Controlled Oscillator (NCO) that generates a cosine and sine wave in the RFSoC's Programmable Logic (PL). We will use the PYNQ Overlay class, DefaultIP class, and DefaultHierarchy class to design drivers for the NCO system. Finally, we will perform visualisation and analysis of the time domain signal and corresponding frequency spectra. Widgets will be used for the purpose of changing the NCO operating parameters during run-time.\n",
    "\n",
    "## Table of Contents\n",
    "* [1. Introduction](#introduction)\n",
    "* [2. The PYNQ-NCO Overlay](#overlays)\n",
    "    * [2.1. The DefaultIP Class](#default-ip-class)\n",
    "    * [2.2. The DefaultHierarchy Class](#default-hierarchy)\n",
    "* [3. Numerically Controlled Oscillator](#nco-example)\n",
    "    * [3.1. NCO Properties](#nco-properties)\n",
    "    * [3.2. Widgets](#widgets)\n",
    "* [4. Visualisation and Analysis](#visualisation-and-analysis)\n",
    "    * [4.1. Time Plot](#time-plot)\n",
    "    * [4.2. Frequency Spectra](#frequency-spectra)\n",
    "* [5. Conclusion](#conclusion)\n",
    "\n",
    "## References\n",
    "* [1] - [StrathSDR, \"PYNQ NCO Overlay\", GitHub Repository.](https://github.com/strath-sdr/pynq_nco)\n",
    "* [2] - [AMD, \"Read the Docs: PYNQ Overlays\", v3.0.0](https://pynq.readthedocs.io/en/v3.0.0/pynq_overlays.html)\n",
    "* [3] - [AMD, \"Read the Docs: pynq.overlay.Overlay\", v3.0.0](https://pynq.readthedocs.io/en/v3.0.0/pynq_package/pynq.overlay.html#pynq.overlay.Overlay)\n",
    "* [4] - [AMD, \"Read the Docs: pynq.overlay.DefaultIP\", v3.0.0](https://pynq.readthedocs.io/en/v3.0.0/pynq_package/pynq.overlay.html#pynq.overlay.DefaultIP)\n",
    "* [5] - [AMD, \"Read the Docs: pynq.overlay.DefaultHierarchy\", v3.0.0](https://pynq.readthedocs.io/en/v3.0.0/pynq_package/pynq.overlay.html#pynq.overlay.DefaultHierarchy)\n",
    "* [6] - [Plotly, \"Plotly Website\", webpage.](https://plotly.com/)\n",
    "* [7] - [jupyter-widgets, “GitHub Source Code Repository for the IPywidgets Python Library,” webpage.](https://github.com/jupyter-widgets/ipywidgets)\n",
    "\n",
    "## Revision\n",
    "* **v1.0** | 23/01/23 | *First Revision*\n",
    "\n",
    "---\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 1. Introduction <a class=\"anchor\" id=\"introduction\"></a>\n",
    "We will use a custom Python module known as PYNQ-NCO [1] extensively in this notebook. The PYNQ-NCO module was developed by the StrathSDR team and contains software code and hardware libraries that implement a Numerically Controlled Oscillator (NCO). NCO's are capable of generating cosine and sine waves using primitive logic elements, such as those that reside in the RFSoC's PL. The PYNQ-NCO example in this notebook will demonstrate how to interact with a PL design using built-in PYNQ classes [2-5] and the visualisation library plotly [6] and interactive widgets library ipywidgets [7].\n",
    "\n",
    "The PYNQ-NCO module was installed automatically when you installed the RFSoC-Book notebooks on your RFSoC platform. See the [PYNQ-NCO GitHub repository](https://github.com/strath-sdr/pynq_nco) if you would like to explore the associated software code and hardware integration libraries on your own."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 2. The PYNQ-NCO Overlay <a class=\"anchor\" id=\"overlays\"></a>\n",
    "We will begin by importing the PYNQ-NCO overlay class, named `NumericalOverlay()`. This class inherits methods and properties from the PYNQ overlay class [3]. During initialisation, the PYNQ overlay class discovers Intellectual Property (IP) Cores and hierarchies that reside in the FPGA design. Drivers are then bound to the IP Cores and hierarchies using PYNQ's built-in DefaultIP [4] and DefaultHierarchy [5] classes, or a user's custom class."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from pynq_nco.overlay import NumericalOverlay\n",
    "\n",
    "ol = NumericalOverlay()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "We can inspect the contents of the design in the FPGA logic fabric of your RFSoC device. The overlay class contains an IP dictionary, which has a description of each hardware accelerator, or IP Core, in the design. This dictionary was created during initialisation. Descriptions of IP Cores were added to the design as they were discovered by the overlay class."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "ol.ip_dict"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "The PYNQ-NCO example contains an NCO IP Core, and a Data Inspector hierarchy block (more on hierarchies in a moment). The Data Inspector consists of two IP Cores known as a Packet Generator and Advanced eXtensible Interface (AXI) Direct Memory Access (DMA). Figure 1 presents the IP integration design for the FPGA logic fabric.\n",
    "\n",
    "<figure>\n",
    "<img src=\"./images/nco_ip_integrator.png\" style=\"width: 100%;\"/>\n",
    "    <figcaption><b>Figure 1: Vivado IP Integrator design for the PYNQ-NCO system.</b></figcaption>\n",
    "</figure>\n",
    "\n",
    "Each IP Core and hierarchy have been assigned a software driver that is responsible for its control and status. These drivers inherit methods and properties from PYNQ's DefaultIP and DefaultHierarchy classes. We will discuss these further below."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 2.1. The DefaultIP Class <a class=\"anchor\" id=\"default-ip-class\"></a>\n",
    "The DefaultIP class is assigned to IP Cores that do not have a more specific driver already assigned. The DefaultIP class provides the user with register read and write capabilities for an IP Core (with an AXI4-Lite interface). The user may choose to inherit the DefaultIP class to build a more specific IP Core driver. For instance, the NCO IP Core has its own class based on the DefaultIP class, which is responsible for configuring the NCO frequency and gain. The methods and properties relating to the software driver for the NCO IP Core can be found running the cell below."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "help(ol.nco)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Notice that the NumericalOscillator class has four methods, two data descriptors, and several inherited methods and properties from the PYNQ DefaultIP class. The DefaultIP class has provided register read and write methods. These have been used to design NCO specific methods and properties that communicate with the FPGA design. Notable custom methods include:\n",
    "\n",
    "```python\n",
    "ol.nco.complex_enable() # Enables the Cosine and Sine wave output of the NCO\n",
    "ol.nco.real_enable()    # Enables the Cosine wave output only of the NCO\n",
    "ol.nco.disable()        # Disables the Cosine and Sine wave output of the NCO\n",
    "```\n",
    "\n",
    "There are also custom properties:\n",
    "\n",
    "```python\n",
    "ol.nco.frequency        # The output frequency of the NCO\n",
    "ol.nco.gain             # The output gain of the NCO\n",
    "```\n",
    "\n",
    "We will use these methods and properties shortly when configuring the output of the NCO. You can inspect the driver source code for the NCO [here](https://github.com/strath-sdr/pynq_nco/blob/main/pynq_nco/nco.py)."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 2.2. The DefaultHierarchy Class <a class=\"anchor\" id=\"default-hierarchy\"></a>\n",
    "Hierarchies are a collection of IP Cores (and/or other hierarchies) that have been grouped together. In the PYNQ-NCO design, there is one hierarchy that is responsible for transferring NCO data from the FPGA to the PS, so that we can plot and visualise its contents. This hierarchy is named the `data_inspector` and it has its own custom driver that inherits the DefaultHierarchy class.\n",
    "\n",
    "The DefaultHierarchy class can be used as the base class for a user to design custom drivers for their own hierarchy. The `data_inspector` hierarchy in the PYNQ-NCO design inherits many useful tools from the DefaultHierarchy including parsers, descriptors, and Partial Reconfiguration (PR) related objects, methods, and properties. We can discover more about the `data_inspector` hierarchy by running the following cell."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "help(ol.data_inspector)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Many of the methods and properties above are related to the DefaultHierarchy class. The `data_inspector` has one significant custom method for moving data between the PL and PS, given below.\n",
    "\n",
    "```python\n",
    "ol.data_inspector.transfer(packetsize)\n",
    "\"\"\"Transfer samples from the FPGA logic fabric to the Processing System.\n",
    "         \n",
    "    Parameters\n",
    "    ----------\n",
    "        packetsize : int\n",
    "            number of samples to transfer\n",
    "\"\"\"\n",
    "```\n",
    "\n",
    "The custom method above will be used later to inspect Cosine and Sine waves generated from the NCO."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 3. Numerically Controlled Oscillator <a class=\"anchor\" id=\"nco-example\"></a>\n",
    "In the analogue world, oscillators are implemented using feedback loops with resonators (often quartz crystal), which control the frequency of oscillations. These oscillators are useful when defining clock signals and sine waveforms. In the digital domain, an equivalent oscillator can be constructed, which is often refferred to as a Numerically Controlled Oscillator, or NCO. NCO's can be implemented using various techniques. A common way is to store samples of a cosine or sine wave in Read-Only Memory (ROM) and read each sample out at a rate corresponding to the desired output frequency.\n",
    "\n",
    "An NCO implemented using the ROM method has several features including a phase input, an accumulator (phase generator) and a ROM for storing values (often implemented using FPGA Look-Up Tables). See the left of Figure 2 for a simplified diagram of this NCO design.\n",
    "\n",
    "<figure>\n",
    "<img src=\"./images/nco_block_diagram.png\" style=\"width: 80%;\"/>\n",
    "    <figcaption><b>Figure 2: Functional block diagram of the NCO design.</b></figcaption>\n",
    "</figure>\n",
    "\n",
    "Also included on the right side of the diagram is the packet generator and AXI Datamover (or AXI DMA) that are present in the `data_inspector` hierarchy. These IP Cores are used to transfer samples of data from the NCO into Jupyter Labs for visualisation.\n",
    "\n",
    "### 3.1. NCO Properties <a class=\"anchor\" id=\"nco-properties\"></a>\n",
    "The NCO architecture above has already been designed and implemented on your RFSoC platform. It is currently operating in the FPGA portion of your RFSoC device. The software driver we defined previously can be used to interact with the input phase and gain stage of the design. Firstly, let us configure the desired frequency of the NCO using the `ol.nco.frequency` property, which appropriately configures the input phase."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "ol.nco.frequency = 12e6 # Hz"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Now, we can also configure the gain of the NCO's output wave."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "ol.nco.gain = 0.5 # Value between -1 and 1"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Lastly, we can either set the NCO to output a cosine wave only, or a cosine and sine wave at the same time. Later we will visualise the output waveform using the `data_inspector` hierarchy."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#ol.nco.real_enable()    # Cosine only output\n",
    "ol.nco.complex_enable() # Cosine and Sine wave output"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 3.2. Widgets <a class=\"anchor\" id=\"widgets\"></a>\n",
    "\n",
    "If desired, we can directly tie the frequency property to a widget that you can interact with. For instance, the frequency can be controlled using a slider."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import ipywidgets as ipw\n",
    "\n",
    "def freq_callback(frequency):\n",
    "    ol.nco.frequency = frequency\n",
    "\n",
    "freq_slider = ipw.FloatSlider(min=-50e6, max=50e6, step=1e6, value=12e6)\n",
    "ipw.interact(freq_callback, frequency=freq_slider);"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "The gain can also be controlled using a slider widget."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def gain_callback(gain):\n",
    "    ol.nco.gain = gain\n",
    "\n",
    "gain_slider = ipw.FloatSlider(min=-1, max=1, step=0.1, value=0.5)\n",
    "ipw.interact(gain_callback, gain=gain_slider);"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Feel free to modify these sliders in any way you like before progressing to the next section."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 4. Visualisation and Analysis <a class=\"anchor\" id=\"visualisation-and-analysis\"></a>\n",
    "The output waveform from the NCO can be transferred and plotted for visualisation and analysis. Before we progress further, let us transfer 1024 samples of data from the FPGA into Jupyter Labs using the `ol.data_inspector.transfer(packetsize)` method of the data inspector hierarchy."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "data = ol.data_inspector.transfer(512)\n",
    "data[0:16] # First 16 samples"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "The samples above are using complex notation as the NCO can output a cosine and sine wave at the same time. If you previously chose to use the cosine wave only, then the imaginary part of the complex number will be 0j. If you chose to use the cosine wave and sine wave, then the real part of the complex number is the cosine wave, and the imaginary part of the complex number refers to the sine wave.\n",
    "\n",
    "We can use the Plotly Graphics Objects library to simply create an interactive plot of NCO data. Import the Plotly library below."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import plotly.graph_objs as go"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 4.2. Time Plot <a class=\"anchor\" id=\"time-plot\"></a>\n",
    "Firstly, we will plot the real and imaginary components of the NCO wave below, using the Plotly Graphics Objects Scatter class. The plot may take a moment to initialise."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "go.Figure(data=[go.Scatter(name=\"Real Data\", y=data.real),\n",
    "                go.Scatter(name=\"Imag Data\", y=data.imag)],\n",
    "          layout={\n",
    "              'title' : 'NCO Time Plot',\n",
    "              'yaxis' : {\n",
    "                  'title' : 'Amplitude'\n",
    "              },\n",
    "              'xaxis' : {\n",
    "                  'title' : 'Samples (n)'\n",
    "              }})"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "You can interact with the plot above. Click and drag over the plot to zoom into a particular region of the waveform. Remember that the imaginary data trace will be zero if you did not select to use the cosine and sine wave output of the NCO."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 4.3. Frequency Spectra <a class=\"anchor\" id=\"frequency-spectra\"></a>\n",
    "We can inspect the output of the NCO in the frequency domain by plotting its corresponding log-scale magnitude spectra. We will use the NumPy Fast Fourier Transform (FFT) module to convert the time domain waveform into its frequency domain equivalent (more on the frequency domain in a later notebook)."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import numpy as np"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Now, we obtain the log-scale magnitude spectra and plot the results using the Plotly Scatter class."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "X = np.fft.fftshift(np.fft.fft(data))\n",
    "X_mag = np.abs(X)\n",
    "X_norm = X_mag/len(X)\n",
    "X_log = 10*np.where(X_norm>0, np.log10(X_norm), 0)\n",
    "\n",
    "go.Figure(data=go.Scatter(y=X_log, x=np.arange(-100e6/2, 100e6/2, 100e6/len(X))),\n",
    "          layout={\n",
    "              'title' : 'NCO Complex Frequency Spectra',\n",
    "              'yaxis' : {\n",
    "                  'title' : 'Log-Scale Magnitude (dB)'\n",
    "              },\n",
    "              'xaxis' : {\n",
    "                  'title' : 'Frequency (Hz)'\n",
    "              }})"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "You should be able to observe a peak in the spectra plot above. The peak will be over the top of the NCO frequency you selected previously. If you chose to use the cosine output of the NCO only, then you will see two peaks in the plot. One will be at the positive NCO frequency and the other will be at the negative NCO frequency. A complex NCO output that consists of cosine and sine waves will only have one peak over the NCO frequency."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 5. Conclusion <a class=\"anchor\" id=\"conclusion\"></a>\n",
    "This notebook has presented a simple NCO overlay for your PYNQ supported platform. We explored the PYNQ overlay, DefaultIP, and DefaultHierarchy built-in classes. Additionally, we investigated an NCO and configured its phase and gain properties. Lastly, we plotted the output waveform of the NCO in time and frequency domains.\n",
    "\n",
    "In the next notebook, we explore discrete sampling."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "---\n",
    "\n",
    "[⬅️ Previous Notebook](03_pynq_introduction.ipynb) || [Next Notebook 🚀](../notebook_B/01_sampling.ipynb)\n",
    "\n",
    "Copyright © 2023 Strathclyde Academic Media\n",
    "\n",
    "---\n",
    "---"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.8.2"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 4
}
